{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "intro",
   "metadata": {},
   "source": [
    "# 数据清洗 - Pandas核心函数\n",
    "\n",
    "## 数据质量审核流程\n",
    "\n",
    "```\n",
    "完整性审核 → 准确性审核 → 一致性审核 → 合理性审核\n",
    "    ↓            ↓            ↓            ↓\n",
    "  缺失值      异常值/格式    数据类型     业务逻辑\n",
    "```\n",
    "\n",
    "---\n",
    "\n",
    "## 一、缺失值检测与处理\n",
    "\n",
    "### 核心函数\n",
    "\n",
    "| 函数 | 功能 | 返回值 | 示例 |\n",
    "|------|------|--------|------|\n",
    "| `isnull()` | 检测缺失值 | 布尔值 | `df.isnull()` |\n",
    "| `notnull()` | 检测非缺失值 | 布尔值 | `df.notnull()` |\n",
    "| `isna()` | 同isnull | 布尔值 | `df.isna()` |\n",
    "| `notna()` | 同notnull | 布尔值 | `df.notna()` |\n",
    "| `dropna()` | 删除缺失值 | DataFrame | `df.dropna()` |\n",
    "| `fillna()` | 填充缺失值 | DataFrame | `df.fillna(0)` |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "missing-detection",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 原始数据 ===\n",
      "     姓名    年龄       工资    部门\n",
      "0    张三  25.0   8000.0    销售\n",
      "1    李四   NaN  12000.0      \n",
      "2  None  32.0      NaN  None\n",
      "3    赵六  28.0  10000.0    技术\n",
      "\n",
      "=== 缺失值检测（布尔矩阵） ===\n",
      "      姓名     年龄     工资     部门\n",
      "0  False  False  False  False\n",
      "1  False   True  False  False\n",
      "2   True  False   True   True\n",
      "3  False  False  False  False\n",
      "\n",
      "=== 每列缺失值统计 ===\n",
      "姓名    1\n",
      "年龄    1\n",
      "工资    1\n",
      "部门    1\n",
      "dtype: int64\n",
      "\n",
      "=== 缺失值比例(%) ===\n",
      "姓名    25.0\n",
      "年龄    25.0\n",
      "工资    25.0\n",
      "部门    25.0\n",
      "dtype: float64\n",
      "\n",
      "=== 包含缺失值的行 ===\n",
      "     姓名    年龄       工资    部门\n",
      "1    李四   NaN  12000.0      \n",
      "2  None  32.0      NaN  None\n"
     ]
    }
   ],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "# 创建示例数据\n",
    "data = {\n",
    "    '姓名': ['张三', '李四', None, '赵六'],\n",
    "    '年龄': [25, None, 32, 28],\n",
    "    '工资': [8000, 12000, np.nan, 10000],\n",
    "    '部门': ['销售', '', None, '技术']\n",
    "}\n",
    "df = pd.DataFrame(data)\n",
    "\n",
    "print(\"=== 原始数据 ===\")\n",
    "print(df)\n",
    "\n",
    "# 1. 检测缺失值\n",
    "print(\"\\n=== 缺失值检测（布尔矩阵） ===\")\n",
    "print(df.isnull())\n",
    "\n",
    "# 2. 统计每列缺失值数量\n",
    "print(\"\\n=== 每列缺失值统计 ===\")\n",
    "print(df.isnull().sum())\n",
    "\n",
    "# 3. 计算缺失值比例\n",
    "print(\"\\n=== 缺失值比例(%) ===\")\n",
    "missing_pct = (df.isnull().sum() / len(df) * 100).round(2)\n",
    "print(missing_pct)\n",
    "\n",
    "# 4. 识别有缺失值的行\n",
    "print(\"\\n=== 包含缺失值的行 ===\")\n",
    "print(df[df.isnull().any(axis=1)])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "handle-missing",
   "metadata": {},
   "source": [
    "### 缺失值处理方法\n",
    "\n",
    "| 方法 | 适用场景 | 代码示例 |\n",
    "|------|----------|----------|\n",
    "| **删除** | 缺失比例小(<5%) | `df.dropna()` |\n",
    "| **填充-固定值** | 分类数据 | `df.fillna('未知')` |\n",
    "| **填充-均值** | 数值数据、正态分布 | `df.fillna(df.mean())` |\n",
    "| **填充-中位数** | 数值数据、有异常值 | `df.fillna(df.median())` |\n",
    "| **填充-众数** | 分类数据 | `df.fillna(df.mode().iloc[0])` |\n",
    "| **前向填充** | 时间序列 | `df.fillna(method='ffill')` |\n",
    "| **后向填充** | 时间序列 | `df.fillna(method='bfill')` |\n",
    "| **插值** | 时间序列、连续数据 | `df.interpolate()` |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "fill-missing",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 删除缺失值 ===\n",
      "   姓名    年龄       工资  部门\n",
      "0  张三  25.0   8000.0  销售\n",
      "3  赵六  28.0  10000.0  技术\n",
      "\n",
      "=== 删除年龄缺失的行 ===\n",
      "     姓名    年龄       工资    部门\n",
      "0    张三  25.0   8000.0    销售\n",
      "2  None  32.0      NaN  None\n",
      "3    赵六  28.0  10000.0    技术\n",
      "\n",
      "=== 填充缺失值 ===\n",
      "   姓名         年龄       工资   部门\n",
      "0  张三  25.000000   8000.0   销售\n",
      "1  李四  28.333333  12000.0     \n",
      "2  未知  32.000000  10000.0  未分配\n",
      "3  赵六  28.000000  10000.0   技术\n"
     ]
    }
   ],
   "source": [
    "# 缺失值处理示例\n",
    "\n",
    "# 1. 删除含缺失值的行\n",
    "print(\"=== 删除缺失值 ===\")\n",
    "df_dropped = df.dropna()\n",
    "print(df_dropped)\n",
    "\n",
    "# 2. 删除特定列的缺失值\n",
    "print(\"\\n=== 删除年龄缺失的行 ===\")\n",
    "df_dropped_age = df.dropna(subset=['年龄'])\n",
    "print(df_dropped_age)\n",
    "\n",
    "# 3. 填充缺失值（推荐写法，避免FutureWarning）\n",
    "print(\"\\n=== 填充缺失值 ===\")\n",
    "df_filled = df.copy()\n",
    "df_filled['姓名'] = df_filled['姓名'].fillna('未知')\n",
    "df_filled['年龄'] = df_filled['年龄'].fillna(df['年龄'].mean())\n",
    "df_filled['工资'] = df_filled['工资'].fillna(df['工资'].median())\n",
    "df_filled['部门'] = df_filled['部门'].fillna('未分配')\n",
    "print(df_filled)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "data-types",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 二、数据类型处理\n",
    "\n",
    "### 类型检查与转换\n",
    "\n",
    "| 函数 | 功能 | 示例 |\n",
    "|------|------|------|\n",
    "| `dtypes` | 查看数据类型 | `df.dtypes` |\n",
    "| `astype()` | 类型转换 | `df['col'].astype(int)` |\n",
    "| `to_numeric()` | 转为数值型 | `pd.to_numeric(df['col'], errors='coerce')` |\n",
    "| `to_datetime()` | 转为日期型 | `pd.to_datetime(df['col'])` |\n",
    "| `select_dtypes()` | 按类型筛选列 | `df.select_dtypes(include=['int64'])` |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "type-conversion",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 原始数据类型 ===\n",
      "数值字符串     object\n",
      "日期字符串     object\n",
      "整数         int64\n",
      "浮点数      float64\n",
      "文本        object\n",
      "dtype: object\n",
      "\n",
      "=== 数值类型列 ===\n",
      "   整数  浮点数\n",
      "0   1  1.1\n",
      "1   2  2.2\n",
      "2   3  3.3\n",
      "3   4  4.4\n",
      "\n",
      "=== 对象类型列 ===\n",
      "     数值字符串       日期字符串 文本\n",
      "0      100  2024-01-01  A\n",
      "1      200  2024-01-02  B\n",
      "2      300  2024-01-03  C\n",
      "3  invalid     invalid  D\n",
      "\n",
      "=== 安全转换数值 ===\n",
      "     数值字符串  数值字符串_转换\n",
      "0      100     100.0\n",
      "1      200     200.0\n",
      "2      300     300.0\n",
      "3  invalid       NaN\n",
      "\n",
      "=== 安全转换日期 ===\n",
      "        日期字符串   日期字符串_转换\n",
      "0  2024-01-01 2024-01-01\n",
      "1  2024-01-02 2024-01-02\n",
      "2  2024-01-03 2024-01-03\n",
      "3     invalid        NaT\n"
     ]
    }
   ],
   "source": [
    "# 创建混合类型数据\n",
    "data = {\n",
    "    '数值字符串': ['100', '200', '300', 'invalid'],\n",
    "    '日期字符串': ['2024-01-01', '2024-01-02', '2024-01-03', 'invalid'],\n",
    "    '整数': [1, 2, 3, 4],\n",
    "    '浮点数': [1.1, 2.2, 3.3, 4.4],\n",
    "    '文本': ['A', 'B', 'C', 'D']\n",
    "}\n",
    "df = pd.DataFrame(data)\n",
    "\n",
    "print(\"=== 原始数据类型 ===\")\n",
    "print(df.dtypes)\n",
    "\n",
    "# 1. 选择数值类型列\n",
    "print(\"\\n=== 数值类型列 ===\")\n",
    "numeric_cols = df.select_dtypes(include=['int64', 'float64'])\n",
    "print(numeric_cols.head())\n",
    "\n",
    "# 2. 选择对象类型列\n",
    "print(\"\\n=== 对象类型列 ===\")\n",
    "object_cols = df.select_dtypes(include=['object'])\n",
    "print(object_cols.head())\n",
    "\n",
    "# 3. 安全的数值转换（无效值转为NaN）\n",
    "print(\"\\n=== 安全转换数值 ===\")\n",
    "df['数值字符串_转换'] = pd.to_numeric(df['数值字符串'], errors='coerce')\n",
    "print(df[['数值字符串', '数值字符串_转换']])\n",
    "\n",
    "# 4. 安全的日期转换\n",
    "print(\"\\n=== 安全转换日期 ===\")\n",
    "df['日期字符串_转换'] = pd.to_datetime(df['日期字符串'], errors='coerce')\n",
    "print(df[['日期字符串', '日期字符串_转换']])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "text-cleaning",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 三、文本数据清洗\n",
    "\n",
    "### 字符串处理函数\n",
    "\n",
    "| 函数 | 功能 | 示例 |\n",
    "|------|------|------|\n",
    "| `str.strip()` | 去除首尾空格 | `df['col'].str.strip()` |\n",
    "| `str.lower()` | 转小写 | `df['col'].str.lower()` |\n",
    "| `str.upper()` | 转大写 | `df['col'].str.upper()` |\n",
    "| `str.replace()` | 替换字符 | `df['col'].str.replace(',', '')` |\n",
    "| `str.contains()` | 检查包含 | `df['col'].str.contains('关键词')` |\n",
    "| `str.len()` | 字符串长度 | `df['col'].str.len()` |\n",
    "| `str.split()` | 分割字符串 | `df['col'].str.split('-')` |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "text-operations",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 原始数据 ===\n",
      "         姓名                 邮箱        金额\n",
      "0       张三   zhang@example.com     1,000\n",
      "1      LISI   LISI@EXAMPLE.COM  2,500.50\n",
      "2       王 五   wang@Example.Com     3,200\n",
      "3  zhao liu   zhao@EXAMPLE.com  4,100.25\n",
      "\n",
      "=== 清洗后数据 ===\n",
      "         姓名                 邮箱        金额     姓名_清洗              邮箱_小写    金额_数值\n",
      "0       张三   zhang@example.com     1,000        张三  zhang@example.com  1000.00\n",
      "1      LISI   LISI@EXAMPLE.COM  2,500.50      LISI   lisi@example.com  2500.50\n",
      "2       王 五   wang@Example.Com     3,200       王 五   wang@example.com  3200.00\n",
      "3  zhao liu   zhao@EXAMPLE.com  4,100.25  zhao liu   zhao@example.com  4100.25\n",
      "\n",
      "=== 检查邮箱包含example ===\n",
      "0    True\n",
      "1    True\n",
      "2    True\n",
      "3    True\n",
      "Name: 邮箱_小写, dtype: bool\n"
     ]
    }
   ],
   "source": [
    "# 文本清洗示例\n",
    "data = {\n",
    "    '姓名': [' 张三 ', 'LISI', '王 五', 'zhao liu'],\n",
    "    '邮箱': ['zhang@example.com', 'LISI@EXAMPLE.COM', 'wang@Example.Com', 'zhao@EXAMPLE.com'],\n",
    "    '金额': ['1,000', '2,500.50', '3,200', '4,100.25']\n",
    "}\n",
    "df = pd.DataFrame(data)\n",
    "\n",
    "print(\"=== 原始数据 ===\")\n",
    "print(df)\n",
    "\n",
    "# 1. 去除空格\n",
    "df['姓名_清洗'] = df['姓名'].str.strip()\n",
    "\n",
    "# 2. 统一大小写\n",
    "df['邮箱_小写'] = df['邮箱'].str.lower()\n",
    "\n",
    "# 3. 去除特殊字符\n",
    "df['金额_数值'] = df['金额'].str.replace(',', '').astype(float)\n",
    "\n",
    "print(\"\\n=== 清洗后数据 ===\")\n",
    "print(df)\n",
    "\n",
    "# 4. 检查包含特定内容\n",
    "print(\"\\n=== 检查邮箱包含example ===\")\n",
    "print(df['邮箱_小写'].str.contains('example'))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "duplicates",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 四、重复值处理\n",
    "\n",
    "### 重复值检测与删除\n",
    "\n",
    "| 函数 | 功能 | 参数 | 示例 |\n",
    "|------|------|------|------|\n",
    "| `duplicated()` | 检测重复行 | `subset`, `keep` | `df.duplicated()` |\n",
    "| `drop_duplicates()` | 删除重复行 | `subset`, `keep` | `df.drop_duplicates()` |\n",
    "\n",
    "**keep参数**：\n",
    "- `'first'`：保留第一次出现（默认）\n",
    "- `'last'`：保留最后一次出现\n",
    "- `False`：删除所有重复项"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "handle-duplicates",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 原始数据 ===\n",
      "   姓名  年龄     工资\n",
      "0  张三  25   8000\n",
      "1  李四  28  12000\n",
      "2  张三  25   8000\n",
      "3  王五  32  15000\n",
      "4  李四  28  13000\n",
      "\n",
      "=== 重复行检测 ===\n",
      "0    False\n",
      "1    False\n",
      "2     True\n",
      "3    False\n",
      "4    False\n",
      "dtype: bool\n",
      "重复行数量: 1\n",
      "\n",
      "=== 姓名列重复检测 ===\n",
      "0    False\n",
      "1    False\n",
      "2     True\n",
      "3    False\n",
      "4     True\n",
      "dtype: bool\n",
      "\n",
      "=== 删除完全重复的行 ===\n",
      "   姓名  年龄     工资\n",
      "0  张三  25   8000\n",
      "1  李四  28  12000\n",
      "3  王五  32  15000\n",
      "4  李四  28  13000\n",
      "\n",
      "=== 基于姓名去重（保留第一个） ===\n",
      "   姓名  年龄     工资\n",
      "0  张三  25   8000\n",
      "1  李四  28  12000\n",
      "3  王五  32  15000\n"
     ]
    }
   ],
   "source": [
    "# 重复值处理示例\n",
    "data = {\n",
    "    '姓名': ['张三', '李四', '张三', '王五', '李四'],\n",
    "    '年龄': [25, 28, 25, 32, 28],\n",
    "    '工资': [8000, 12000, 8000, 15000, 13000]\n",
    "}\n",
    "df = pd.DataFrame(data)\n",
    "\n",
    "print(\"=== 原始数据 ===\")\n",
    "print(df)\n",
    "\n",
    "# 1. 检测完全重复的行\n",
    "print(\"\\n=== 重复行检测 ===\")\n",
    "print(df.duplicated())\n",
    "print(f\"重复行数量: {df.duplicated().sum()}\")\n",
    "\n",
    "# 2. 检测特定列的重复\n",
    "print(\"\\n=== 姓名列重复检测 ===\")\n",
    "print(df.duplicated(subset=['姓名']))\n",
    "\n",
    "# 3. 删除完全重复的行\n",
    "print(\"\\n=== 删除完全重复的行 ===\")\n",
    "df_unique = df.drop_duplicates()\n",
    "print(df_unique)\n",
    "\n",
    "# 4. 基于特定列删除重复（保留第一个）\n",
    "print(\"\\n=== 基于姓名去重（保留第一个） ===\")\n",
    "df_unique_name = df.drop_duplicates(subset=['姓名'], keep='first')\n",
    "print(df_unique_name)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "outliers",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 五、异常值检测\n",
    "\n",
    "### 统计方法检测异常值\n",
    "\n",
    "| 方法 | 公式 | 适用场景 |\n",
    "|------|------|----------|\n",
    "| **IQR方法** | Q1-1.5×IQR, Q3+1.5×IQR | 通用，对偏态分布适用 |\n",
    "| **Z-score** | \\|z\\| > 3 | 正态分布数据 |\n",
    "| **业务规则** | 自定义阈值 | 有明确业务界限 |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "detect-outliers",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 原始数据 ===\n",
      "       工资\n",
      "0    5000\n",
      "1    6000\n",
      "2    7000\n",
      "3    5500\n",
      "4    6500\n",
      "5  100000\n",
      "6    7200\n",
      "7    5800\n",
      "8   -1000\n",
      "9    6200\n",
      "\n",
      "=== IQR方法 ===\n",
      "Q1 (25%): 5575.0\n",
      "Q3 (75%): 6875.0\n",
      "IQR: 1300.0\n",
      "正常范围: [3625.0, 8825.0]\n",
      "\n",
      "=== 异常值标记 ===\n",
      "       工资   是否异常\n",
      "0    5000  False\n",
      "1    6000  False\n",
      "2    7000  False\n",
      "3    5500  False\n",
      "4    6500  False\n",
      "5  100000   True\n",
      "6    7200  False\n",
      "7    5800  False\n",
      "8   -1000   True\n",
      "9    6200  False\n",
      "\n",
      "=== 异常值列表 ===\n",
      "       工资  是否异常\n",
      "5  100000  True\n",
      "8   -1000  True\n",
      "\n",
      "=== 业务规则检测（工资应在3000-50000之间） ===\n",
      "       工资  是否异常  业务规则异常\n",
      "5  100000  True    True\n",
      "8   -1000  True    True\n"
     ]
    }
   ],
   "source": [
    "# 异常值检测示例\n",
    "data = {\n",
    "    '工资': [5000, 6000, 7000, 5500, 6500, 100000, 7200, 5800, -1000, 6200]\n",
    "}\n",
    "df = pd.DataFrame(data)\n",
    "\n",
    "print(\"=== 原始数据 ===\")\n",
    "print(df)\n",
    "\n",
    "# 方法1: IQR方法检测异常值\n",
    "Q1 = df['工资'].quantile(0.25)\n",
    "Q3 = df['工资'].quantile(0.75)\n",
    "IQR = Q3 - Q1\n",
    "lower_bound = Q1 - 1.5 * IQR\n",
    "upper_bound = Q3 + 1.5 * IQR\n",
    "\n",
    "print(f\"\\n=== IQR方法 ===\")\n",
    "print(f\"Q1 (25%): {Q1}\")\n",
    "print(f\"Q3 (75%): {Q3}\")\n",
    "print(f\"IQR: {IQR}\")\n",
    "print(f\"正常范围: [{lower_bound}, {upper_bound}]\")\n",
    "\n",
    "# 标记异常值\n",
    "df['是否异常'] = (df['工资'] < lower_bound) | (df['工资'] > upper_bound)\n",
    "print(\"\\n=== 异常值标记 ===\")\n",
    "print(df)\n",
    "\n",
    "# 筛选异常值\n",
    "print(\"\\n=== 异常值列表 ===\")\n",
    "outliers = df[df['是否异常']]\n",
    "print(outliers)\n",
    "\n",
    "# 方法2: 业务规则检测\n",
    "print(\"\\n=== 业务规则检测（工资应在3000-50000之间） ===\")\n",
    "df['业务规则异常'] = (df['工资'] < 3000) | (df['工资'] > 50000)\n",
    "print(df[df['业务规则异常']])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "summary",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 六、数据清洗速查表\n",
    "\n",
    "### 完整清洗流程\n",
    "\n",
    "```python\n",
    "# 1. 查看数据概况\n",
    "df.info()              # 数据信息\n",
    "df.describe()          # 统计描述\n",
    "df.head()              # 前几行\n",
    "\n",
    "# 2. 缺失值处理\n",
    "df.isnull().sum()      # 统计缺失值\n",
    "df.dropna()            # 删除缺失值\n",
    "df.fillna(value)       # 填充缺失值\n",
    "\n",
    "# 3. 重复值处理\n",
    "df.duplicated().sum()  # 统计重复值\n",
    "df.drop_duplicates()   # 删除重复值\n",
    "\n",
    "# 4. 数据类型处理\n",
    "df.dtypes              # 查看类型\n",
    "df.astype()            # 类型转换\n",
    "pd.to_numeric()        # 转数值\n",
    "pd.to_datetime()       # 转日期\n",
    "\n",
    "# 5. 文本清洗\n",
    "df['col'].str.strip()   # 去空格\n",
    "df['col'].str.lower()   # 转小写\n",
    "df['col'].str.replace() # 替换字符\n",
    "\n",
    "# 6. 异常值处理\n",
    "# IQR方法或业务规则\n",
    "```\n",
    "\n",
    "---\n",
    "\n",
    "## 小结\n",
    "\n",
    "**核心函数分类**：\n",
    "1. **缺失值**：`isnull()`, `dropna()`, `fillna()`\n",
    "2. **重复值**：`duplicated()`, `drop_duplicates()`\n",
    "3. **数据类型**：`astype()`, `to_numeric()`, `to_datetime()`, `select_dtypes()`\n",
    "4. **文本处理**：`str.strip()`, `str.lower()`, `str.replace()`\n",
    "5. **异常值**：IQR方法、Z-score、业务规则\n",
    "\n",
    "**下一步**：在实际数据集上应用这些函数进行数据清洗实操"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:base] *",
   "language": "python",
   "name": "conda-base-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
